package zredis

import (
	"context"
	"crypto/tls"
	"errors"
	"strconv"
	"sync/atomic"
	"time"

	log "gitee.com/youkelike/zlog"
	redis "github.com/redis/go-redis/v9"
	uuid "github.com/satori/go.uuid"
)

type Config struct {
	Host                  string
	Port                  int
	Addrs                 []string
	MasterName            string
	Username              string
	Password              string
	Database              int
	MaxIdle               int
	MaxActive             int
	Timeout               int
	EnableCluster         bool
	UseSSL                bool
	SSLInsecureSkipVerify bool
}

var ErrRedisIsDown = errors.New("storage: Redis is either down or ws not configured")

var (
	singlePool atomic.Value
	// redis 连接池是否可用
	redisUp atomic.Value
	// 是否禁用 redis
	disableRedis atomic.Value
)

// 关闭 redis 连接
func DisableRedis(ok bool) {
	if ok {
		redisUp.Store(false)
		disableRedis.Store(true)
		return
	}

	redisUp.Store(true)
	disableRedis.Store(false)
}

// 是否要重新建立连接
func shouldConnect() bool {
	if v := disableRedis.Load(); v != nil {
		return !v.(bool)
	}

	return true
}

// 是否已建立连接
func connected() bool {
	if v := redisUp.Load(); v != nil {
		return v.(bool)
	}

	return false
}

// 拿到单例
func singleton() redis.UniversalClient {
	if v := singlePool.Load(); v != nil {
		return v.(redis.UniversalClient)
	}
	return nil
}

// 先尝试拿单例，没拿到就实例化后存入，这个方法不会被并发调用，所以不用加锁
func connectSingleton(config *Config) bool {
	client := singleton()
	if client == nil {
		client := NewRedisClusterPool(config)
		singlePool.Store(client)
	}
	return true
}

// 每隔 1 秒检查 redis 连接池是否可用，不可用会自动尝试重连
// 这个方法应该只在启动流程中被调用一次
func ConnectToRedis(ctx context.Context, config *Config) {
	tick := time.NewTicker(time.Second)
	defer tick.Stop()

	c := RedisCluster{}
	ok := true
	if !connectSingleton(config) {
		ok = false
	}
	if !clusterConnectionIsOpen(c) {
		ok = false
	}
	redisUp.Store(ok)

again:
	for {
		select {
		case <-ctx.Done():
			return
		case <-tick.C:
			// 如果禁用了 redis，这里就跳过后面的操作了
			if !shouldConnect() {
				continue
			}

			if !connectSingleton(config) {
				// 只需要标记不可用，等待连接池自动恢复
				redisUp.Store(false)
				goto again
			}

			if !clusterConnectionIsOpen(c) {
				// 只需要标记不可用，等待连接池自动恢复
				redisUp.Store(false)
				goto again
			}
			redisUp.Store(true)
		}
	}
}

// 测试 redis 连接是否可用
func clusterConnectionIsOpen(cluster RedisCluster) bool {
	c := singleton()
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
	defer cancel()

	testKey := "redis-test-" + uuid.Must(uuid.NewV4()).String()
	if err := c.Set(ctx, testKey, "test", time.Second).Err(); err != nil {
		log.Warnf("Error trying to set test key: %s", err.Error())
		return false
	}

	if _, err := c.Get(ctx, testKey).Result(); err != nil {
		log.Warnf("Error trying to get test key: %s", err.Error())
		return false
	}
	return true
}

// 这里只是拿到 redis 连接池对象，还没有真的连接数据库，所以不用返回 error
// cache 参数实际没用到
func NewRedisClusterPool(config *Config) redis.UniversalClient {

	log.Debugw("create new redis connection pool")

	poolSize := 500
	if config.MaxActive > 0 {
		poolSize = config.MaxActive
	}

	timeout := 5 * time.Second
	if config.Timeout > 0 {
		timeout = time.Duration(config.Timeout) * time.Second
	}

	var tlsConfig *tls.Config
	if config.UseSSL {
		tlsConfig = &tls.Config{
			InsecureSkipVerify: config.SSLInsecureSkipVerify,
		}
	}

	var client redis.UniversalClient
	opts := &RedisOpts{
		Addrs:           getRedisAddrs(config),
		MasterName:      config.MasterName,
		Password:        config.Password,
		DB:              config.Database,
		DialTimeout:     timeout,
		ReadTimeout:     timeout,
		WriteTimeout:    timeout,
		ConnMaxIdleTime: 240 * timeout,
		PoolSize:        poolSize,
		TLSConfig:       tlsConfig,
	}
	if opts.MasterName != "" {
		log.Infow("--> [REDIS] Creating sentinel-backed failover client")
		// 连接到哨兵
		client = redis.NewFailoverClient(opts.failover())
	} else if config.EnableCluster {
		log.Infow("--> [REDIS] Creating cluster client")
		// 连接到集群
		client = redis.NewClusterClient(opts.cluster())
	} else {
		log.Infow("--> [REDIS] Creating single-node client")
		// 普通连接池
		client = redis.NewClient(opts.simple())
	}

	return client
}

func getRedisAddrs(config *Config) (addrs []string) {
	if len(config.Addrs) != 0 {
		addrs = config.Addrs
	}

	if len(addrs) == 0 && config.Port != 0 {
		addr := config.Host + ":" + strconv.Itoa(config.Port)
		addrs = append(addrs, addr)
	}

	return addrs
}

type RedisOpts redis.UniversalOptions

func (o *RedisOpts) cluster() *redis.ClusterOptions {
	if len(o.Addrs) == 0 {
		o.Addrs = []string{"127.0.0.1:6379"}
	}

	return &redis.ClusterOptions{
		Addrs:     o.Addrs,
		OnConnect: o.OnConnect,

		Password: o.Password,

		MaxRedirects:   o.MaxRedirects,
		ReadOnly:       o.ReadOnly,
		RouteByLatency: o.RouteByLatency,
		RouteRandomly:  o.RouteRandomly,

		MaxRetries:      o.MaxRetries,
		MinRetryBackoff: o.MinRetryBackoff,
		MaxRetryBackoff: o.MaxRetryBackoff,

		DialTimeout:     o.DialTimeout,
		ReadTimeout:     o.ReadTimeout,
		WriteTimeout:    o.WriteTimeout,
		PoolSize:        o.PoolSize,
		MinIdleConns:    o.MinIdleConns,
		ConnMaxLifetime: o.ConnMaxLifetime,
		PoolTimeout:     o.PoolTimeout,
		ConnMaxIdleTime: o.ConnMaxIdleTime,

		TLSConfig: o.TLSConfig,
	}
}

func (o *RedisOpts) failover() *redis.FailoverOptions {
	if len(o.Addrs) == 0 {
		o.Addrs = []string{"127.0.0.1:26379"}
	}

	return &redis.FailoverOptions{
		SentinelAddrs: o.Addrs,
		MasterName:    o.MasterName,
		OnConnect:     o.OnConnect,

		DB:       o.DB,
		Password: o.Password,

		MaxRetries:      o.MaxRetries,
		MinRetryBackoff: o.MinRetryBackoff,
		MaxRetryBackoff: o.MaxRetryBackoff,

		DialTimeout:  o.DialTimeout,
		ReadTimeout:  o.ReadTimeout,
		WriteTimeout: o.WriteTimeout,

		PoolSize:        o.PoolSize,
		MinIdleConns:    o.MinIdleConns,
		ConnMaxLifetime: o.ConnMaxLifetime,
		PoolTimeout:     o.PoolTimeout,
		ConnMaxIdleTime: o.ConnMaxIdleTime,

		TLSConfig: o.TLSConfig,
	}
}

func (o *RedisOpts) simple() *redis.Options {
	addr := "127.0.0.1:6379"
	if len(o.Addrs) > 0 {
		addr = o.Addrs[0]
	}

	return &redis.Options{
		Addr:      addr,
		OnConnect: o.OnConnect,

		DB:       o.DB,
		Password: o.Password,

		MaxRetries:      o.MaxRetries,
		MinRetryBackoff: o.MinRetryBackoff,
		MaxRetryBackoff: o.MaxRetryBackoff,

		DialTimeout:  o.DialTimeout,
		ReadTimeout:  o.ReadTimeout,
		WriteTimeout: o.WriteTimeout,

		PoolSize:        o.PoolSize,
		MinIdleConns:    o.MinIdleConns,
		ConnMaxLifetime: o.ConnMaxLifetime,
		PoolTimeout:     o.PoolTimeout,
		ConnMaxIdleTime: o.ConnMaxIdleTime,

		TLSConfig: o.TLSConfig,
	}
}
